package com.jeesuite.springweb.exporter;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.jeesuite.common.GlobalRuntimeContext;
import com.jeesuite.common.annotation.ApiMetadata;
import com.jeesuite.common.constants.PermissionLevel;
import com.jeesuite.common.model.ApiInfo;
import com.jeesuite.common.util.ResourceUtils;
import com.jeesuite.springweb.model.AppMetadata;

import io.swagger.annotations.ApiOperation;

/**
 * @description <br>
 * @author <a href="mailto:vakinge@gmail.com">vakin</a>
 * @date 2018年4月17日
 */
public class AppMetadataHolder {

	private static AppMetadata metadata;

	private synchronized static void scanApiInfos(AppMetadata metadata, List<String> classNameList) {

		if (!metadata.getApis().isEmpty())
			return;

		Method[] methods;
		String baseUri;
		ApiInfo apiInfo;
		ApiMetadata classMetadata;
		ApiMetadata methodMetadata;
		for (String className : classNameList) {
			if (!className.contains(GlobalRuntimeContext.MODULE_NAME))
				continue;
			try {
				Class<?> clazz = Class.forName(className);
				if (!clazz.isAnnotationPresent(Controller.class) && !clazz.isAnnotationPresent(RestController.class)) {
					continue;
				}

				RequestMapping requestMapping = AnnotationUtils.findAnnotation(clazz, RequestMapping.class);
				if (requestMapping != null) {
					baseUri = requestMapping.value()[0];
					if (!baseUri.startsWith("/"))
						baseUri = "/" + baseUri;
					if (baseUri.endsWith("/"))
						baseUri = baseUri.substring(0, baseUri.length() - 1);
				} else {
					baseUri = "";
				}
				//
				classMetadata = clazz.getAnnotation(ApiMetadata.class);
				methods = clazz.getDeclaredMethods();
				Map<String, Method> interfaceMethods = getInterfaceMethods(clazz);
				methodLoop: for (Method method : methods) {
					methodMetadata = method.isAnnotationPresent(ApiMetadata.class)
							? method.getAnnotation(ApiMetadata.class)
							: classMetadata;
					String apiUri = null;
					String apiHttpMethod = null;

					requestMapping = getAnnotation(method, interfaceMethods.get(method.getName()),
							RequestMapping.class);
					if (requestMapping != null) {
						apiUri = requestMapping.value()[0];
						if (requestMapping.method() != null && requestMapping.method().length > 0) {
							apiHttpMethod = requestMapping.method()[0].name();
						}
					} else {
						PostMapping postMapping = getAnnotation(method, interfaceMethods.get(method.getName()),
								PostMapping.class);
						if (postMapping != null) {
							apiUri = postMapping.value()[0];
							apiHttpMethod = RequestMethod.POST.name();
						}
						GetMapping getMapping = getAnnotation(method, interfaceMethods.get(method.getName()),
								GetMapping.class);
						if (getMapping != null) {
							apiUri = getMapping.value()[0];
							apiHttpMethod = RequestMethod.GET.name();
						}
					}

					if (StringUtils.isBlank(apiUri)) {
						continue methodLoop;
					}

					apiInfo = new ApiInfo();
					if (apiUri == null) {
						apiUri = baseUri;
					} else {
						if (!apiUri.startsWith("/")) {
							apiUri = "/" + apiUri;
						}
						apiUri = baseUri + apiUri;
					}
					apiInfo.setUrl(apiUri);
					apiInfo.setMethod(apiHttpMethod);

					if (method.isAnnotationPresent(ApiOperation.class)) {
						apiInfo.setName(method.getAnnotation(ApiOperation.class).value());
					} else {
						apiInfo.setName(apiInfo.getUrl());
					}
					
					if (methodMetadata != null) {
						apiInfo.setActionLog(methodMetadata.actionLog());
						apiInfo.setRequestLog(methodMetadata.requestLog());
						apiInfo.setResponseLog(methodMetadata.responseLog());
					}

					if (methodMetadata != null && StringUtils.isNotBlank(methodMetadata.actionName())) {
						apiInfo.setName(methodMetadata.actionName());
					} else if (method.isAnnotationPresent(ApiOperation.class)) {
						apiInfo.setName(method.getAnnotation(ApiOperation.class).value());
					}

					if (methodMetadata == null) {
						apiInfo.setPermissionType(PermissionLevel.LoginRequired);
					} else {
						apiInfo.setPermissionType(methodMetadata.permissionLevel());
					}
					metadata.getApis().add(apiInfo);
				}
			} catch (Exception e) {
				System.err.println("error className:" + className);
			}
		}
	}

	private static Map<String, Method> getInterfaceMethods(Class<?> clazz) {
		Map<String, Method> map = new HashMap<>();
		Class<?>[] interfaces = clazz.getInterfaces();
		if (interfaces == null)
			return map;
		for (Class<?> class1 : interfaces) {
			Method[] methods = class1.getDeclaredMethods();
			for (Method method : methods) {
				map.put(method.getName(), method);
			}
		}

		return map;
	}

	private static <T extends Annotation> T getAnnotation(Method classMethod, Method interfaceMethod,
			Class<T> annotationClass) {
		T annotation = classMethod.getAnnotation(annotationClass);
		if (annotation == null && interfaceMethod != null) {
			annotation = interfaceMethod.getAnnotation(annotationClass);
		}

		return annotation;
	}

	public static AppMetadata getMetadata() {
		if (metadata != null)
			return metadata;
		synchronized (AppMetadataHolder.class) {
			//
			metadata = new AppMetadata();
			metadata.setServiceId(GlobalRuntimeContext.APPID);

			String basePackage = ResourceUtils.getProperty("jeesuite.application.base-package");
			if (basePackage == null)
				return metadata;
			List<String> classNameList = scanControllerClassNames(basePackage);
			//
			scanApiInfos(metadata, classNameList);

			if (ResourceUtils.containsProperty("dependency.services")) {
				metadata.setDependencyServices(ResourceUtils.getList("dependency.services"));
			}

		}
		return metadata;
	}

	private static List<String> scanControllerClassNames(String basePackage) {

		List<String> result = new ArrayList<>();

		String RESOURCE_PATTERN = "/**/*.class";

		ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
		try {
			String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
					+ ClassUtils.convertClassNameToResourcePath(basePackage) + RESOURCE_PATTERN;
			org.springframework.core.io.Resource[] resources = resourcePatternResolver.getResources(pattern);
			MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
			for (org.springframework.core.io.Resource resource : resources) {
				if (resource.isReadable()) {
					MetadataReader reader = readerFactory.getMetadataReader(resource);
					String className = reader.getClassMetadata().getClassName();
					Class<?> clazz = Class.forName(className);
					if (clazz.isAnnotationPresent(Controller.class)
							|| clazz.isAnnotationPresent(RestController.class)) {
						result.add(clazz.getName());
					}
				}
			}
		} catch (Exception e) {
		}

		return result;

	}

}
